package com.deimos_space.sps_cfi.jni;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.vast.util.DateTime;
import org.vast.util.DateTimeFormat;

import com.deimos.dataBase.Sensor;
import com.deimos.dataBase.TaskController;

import edu.emory.mathcs.backport.java.util.Collections;

/**
 * Performs feasibility studies using the CFIs
 * 
 * @author Manuel Casado Martin TODO: Handle CFIs exceptions TODO: check input
 *         data (e.g. size of lat and lon arrays)
 */
public class Planner {

    /**
     * Latitudes of the region of interest
     */
    private double[] regionOfInterest_lat;

    /**
     * Longitudes of the region of interest
     */
    private double[] regionOfInterest_lon;

    /**
     * Survey period expressed as an array of two dates in the following format
     * <i>YYYY-MM-DD_HH:MM:SS.mmmmmm</i>
     */
    private String[] surveyPeriod;

    /**
     * Elevation Incidence Angle expressed as an array of two values: minimum
     * angle and maximum angle E.g.: <i>[20.0, 30.0]</i>
     */
    private double elevationIncidenceAngle[];

    /**
     * Token to describe a particular instrument configuration. E.g. <b>EW</b>
     */
    private String instrumentMode;

    /**
     * Receive/Transmit polarization mode of a SAR instrument. E.g. <i>HH/HV</i>
     */
    private String polarizationMode;

    /**
     * Complete path of the swath file. E.g.
     * <i>/home/HMA/ropFiles/SSC1_R_____TS_L_SWATH</i>
     */
    private String swathFile;

    /**
     * Wrapper to access the CFI libraries through JNI
     */
    public static CFIWrapperJNI cfiw = null;

    /**
     * Flag to enable/disable printouts
     */
    private boolean messages = false;

    private static boolean isInUse = false;

    double minRange, maxRange;
    int occurrences;
    SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd_HH:mm:ss.SSSSSS");
    SimpleDateFormat DATEFORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
    String taskId = "";

    private Sensor sensor;

    /**
     * @param sensorID
     *            Identifies the sensor that shall be tasked
     * @param regionOfInterest_lat
     *            Latitudes of the region of interest
     * @param regionOfInterest_lon
     *            Longitudes of the region of interest
     * @param surveyPeriod
     *            Survey period expressed as an array of two dates in the
     *            following format <i>YYYY-MM-DD_HH:MM:SS.mmmmmm</i>
     * @param elevationIncidenceAngle
     *            Elevation Incidence Angle expressed as an array of two values:
     *            minimum angle and maximum angle. E.g.: [20.0, 30.0]
     * @param instrumentMode
     *            Token to describe a particular instrument configuration. E.g.
     *            <b>EW</b>
     * @param polarizationMode
     *            Receive/Transmit polarization mode of a SAR instrument. E.g.
     *            <i>HH/HV</i>
     * @param swathFile
     *            Complete path of the swath file to use
     * @param osfFile
     *            Complete path of the OSF file. E.g.
     *            <i>/home/HMA/ropFiles/OSF_TS_L</i>
     */

    public Planner() {
    }

    public boolean getIsInUse() {
	return isInUse;
    }

    public void initPlanner(Sensor sensor, double[] regionOfInterest_lat, double[] regionOfInterest_lon, String[] surveyPeriod, double[] elevationIncidenceAngle, String instrumentMode, String polarizationMode, double maxRange, double minRange, int occurrences, String taskId) {
	this.regionOfInterest_lat = regionOfInterest_lat;
	this.regionOfInterest_lon = regionOfInterest_lon;
	this.surveyPeriod = surveyPeriod;
	this.elevationIncidenceAngle = elevationIncidenceAngle;
	this.instrumentMode = instrumentMode;
	this.polarizationMode = polarizationMode;
	this.sensor = sensor;
	this.swathFile = this.sensor.getSwaths(this.instrumentMode);
	this.minRange = minRange;
	this.maxRange = maxRange;
	this.occurrences = occurrences;
	this.taskId = taskId;
    }

    /**
     * Performs a feasibility study based on the current stored study
     * parameters.
     * 
     * @param basicStudy
     *            If set to <b>true</b> only the first mapping is consider. If
     *            set to <b>false</b> all the mappings are processed. This
     *            functionality is not yet implemented.
     * @return Feasibility study
     * @throws Exception
     */
    public FeasibilityStudy studyFeasibility(boolean basicStudy) {
	FeasibilityStudy study = null;

	if (loadLib())
	    study = studyFeasibility();
	else {
	    study = new FeasibilityStudy();
	    study.setResult(Feasibility.NOT_FEASIBLE);
	    study.setDescription("CFI Library can not be loaded");
	    if (messages)
		System.out.println("CFI Library can not be loaded");
	}

	this.unloadLib();

	return study;
    }

    public boolean loadLib() {
	while (isInUse)
	    ;

	try {
	    if (Planner.cfiw == null) {
		if (messages) System.out.println("Loading CFI Library");
		Planner.cfiw = new CFIWrapperJNI();
	    } else {
		Planner.cfiw = null;
		System.gc();
		loadLib();
	    }
	} catch (Exception ex) {
	    ex.printStackTrace();
	}

	try {

	    if (cfiw != null && sensor.getOsf_file() != null) {
		cfiw.Init(sensor.getOsf_file());
		isInUse = true;
		return true;
	    }
	} catch (Exception ex) {
	    ex.printStackTrace();
	}
	return false;
    }

    public void unloadLib() {
	isInUse = false;
    }

    /**
     * Performs a feasibility study based on the current stored study
     * parameters.
     * 
     * @return Feasibility study
     * @throws ParseException
     * @throws Exception
     */
    private FeasibilityStudy studyFeasibility() {
	FeasibilityStudy study = new FeasibilityStudy();
	try {
	    study.setResult(Feasibility.NOT_FEASIBLE);
	    study.setDescription("Task not feasible. ");

	    // Check that all the required params (e.g. OSF file) are properly
	    // set
	    assertFeasibilityParams();

	    //
	    // Perform feasibility study
	    //
	    int zonePoints = regionOfInterest_lat.length;

	    // TODO: Parameterize the following harcoded values
	    double overlapPercentage = 1.;
	    long direction = 2;
	    double diameter = 0.;

	    if (messages) {
		System.out.println("ZONE MAPPING STUDY:");
		System.out.println("-------------------------------------------");

		System.out.println(">>> Input values:");
		System.out.println("Times: " + this.surveyPeriod[0] + ", " + this.surveyPeriod[1]);
		System.out.println("Swath: " + this.getSwathName(this.getSwathFile()));
		System.out.println("Overlap: " + overlapPercentage);
		System.out.println("Direction: " + direction);
		System.out.println("Zone: ");
		for (int n = 0; n < zonePoints; n++) {
		    System.out.println(this.regionOfInterest_lon[n] + ", " + this.regionOfInterest_lat[n]);
		}
	    }


	    // VALIDATE POLYGON
	    for (int i = 0; i < regionOfInterest_lat.length; i++) {
		for (int j = i + 1; j < regionOfInterest_lat.length; j++) {
		    if (regionOfInterest_lat[i] == regionOfInterest_lat[j] && regionOfInterest_lon[i] == regionOfInterest_lon[j]) {
			study.setResult(Feasibility.NOT_FEASIBLE);
			study.setDescription("Incorrect Polygon");
			return study;
		    }
		}
	    }

	    Date lastsegment = null;
	    if (occurrences <= 0)
		occurrences = 1;
	    for (int i = 0; i < occurrences; i++) {
		long err;
		String[] segStart = new String[999];
		String[] segStop = new String[999];
		long[] nbSegments = new long[99];
		double[] coverage = new double[999];
		long nbMappings = 10;
		// GET ZONE MAPPING

		Date start = null, stop = null, startBefore = null;

		start = sdf.parse(this.surveyPeriod[0]);

		stop = sdf.parse(this.surveyPeriod[1]);

		if (lastsegment == null) {
		    start = sdf.parse(this.surveyPeriod[0]);
		} else {
		    start = new Date(lastsegment.getTime() + ((long) minRange * 24 * 60 * 60 * 1000));
		    if (maxRange != 0)
			startBefore = new Date(lastsegment.getTime() + ((long) maxRange * 24 * 60 * 60 * 1000));
		    // stop = new Date(lastsegment.getTime() + ((long) maxRange
		    // * 24 * 60 * 60 * 1000));
		}

		if (stop.after(sdf.parse(this.surveyPeriod[1]))) {
		    stop = sdf.parse(this.surveyPeriod[1]);
		}

		if (start.after(stop)) {
		    study.setResult(Feasibility.NOT_FEASIBLE);
		    study.setDescription("Task not feasible. The number of segments is 0 for the occurrence " + (i + 1) + ". ");
		    return study;
		}

		if (messages) System.out.println("Dates: " + sdf.format(start) + " , " + sdf.format(stop));

		err = cfiw.GetZoneMapping(sdf.format(start), sdf.format(stop), this.swathFile, overlapPercentage, direction, this.regionOfInterest_lat, this.regionOfInterest_lon, this.regionOfInterest_lat.length, diameter, // Output
		// value
		segStart, // Output value
		segStop, // Output value
		nbSegments, // Output value
		coverage); // Output value

		if (err != 0) {
		    regionOfInterest_lon = this.reverse(regionOfInterest_lon);
		    regionOfInterest_lat = this.reverse(regionOfInterest_lat);

		    err = cfiw.GetZoneMapping(sdf.format(start), sdf.format(stop), this.swathFile, overlapPercentage, direction, this.regionOfInterest_lat, this.regionOfInterest_lon, this.regionOfInterest_lat.length, diameter, // Output
		    // value
		    segStart, // Output value
		    segStop, // Output value
		    nbSegments, // Output value
		    coverage); // Output value

		}

		if (err != 0) {
		    study.setResult(Feasibility.NOT_FEASIBLE);
		    study.setDescription("Feasibility cannot be checked as the CFI Library cannot map this zone");
		    return study;
		}

		if (messages) {

		    System.out.println("ZONE MAPPING STUDY:");
		    System.out.println("-------------------------------------------");
		    System.out.println(">>> Input values:");
		    System.out.println("Times: " + this.surveyPeriod[0] + ", " + this.surveyPeriod[1]);
		    System.out.println("Swath: " + this.swathFile);
		    System.out.println("Overlap: " + overlapPercentage);
		    System.out.println("Direction: " + direction);
		    System.out.println("Zone: ");

		    for (int n = 0; n < zonePoints; n++) {
			System.out.println(this.regionOfInterest_lon[n] + ", " + this.regionOfInterest_lat[n]);
		    }
		}

		//
		// If the number of mappings obtained is 0 the study is NOT
		// FEASIBLE
		//
		if (nbSegments[0] == 0) {
		    study.setResult(Feasibility.NOT_FEASIBLE);
		    study.setDescription("Task not feasible. The number of segments is 0 for the occurrence " + (i + 1) + ". ");
		    return study;
		}

		if (messages) {
		    System.out.println("<<< Output:");
		    System.out.println("Number of mappings: " + nbSegments[0]);
		}
		nbMappings = nbSegments[0];

		//
		// Process the mapping(s) to get the swath points
		//

		boolean valid = false;
		List<String> tasks = new ArrayList<String>();
		int segId = 0;
		for (int nMapping = 0; nMapping < nbMappings && !valid; nMapping++) {
		    if (messages) {
			System.out.println("MAPPING " + nMapping);
		    }

		    valid = true;
		    for (int m = 0; m < nbSegments[nMapping + 1] && valid; m++) {
			String[] segment = new String[3];
			Date segmentStart = sdf.parse(segStart[segId]);
			Date segmentStop = sdf.parse(segStop[segId]);
			if (segmentStart.after(segmentStop)) {
			    segment[0] = segStop[segId];
			    segment[1] = segStart[segId];
			    segmentStart = sdf.parse(segStop[segId]);
			    segmentStop = sdf.parse(segStart[segId]);
			} else {
			    segment[0] = segStart[segId];
			    segment[1] = segStop[segId];
			}

			TaskController taskController = new TaskController();
			segId++;
			for (String task : taskController.isSensorInUse(sensor.getSensorId(), segmentStart, segmentStop, taskId)) {
			    if (Collections.binarySearch(tasks, task) < 0)
				tasks.add(task);
			    valid = false;
			}
		    }

		    if (startBefore != null && startBefore.before(sdf.parse(segStart[(int) (segId - nbSegments[nMapping + 1])])))
			valid = false;

		    if (valid)
			segId = (int) (segId - nbSegments[nMapping + 1]);

		    for (int m = 0; m < nbSegments[nMapping + 1] && valid; m++) {
			if (messages) {
			    System.out.println("SEGMENT: " + m);
			    System.out.println(segStart[segId] + " , " + segStop[segId] + " , " + coverage[(int) nbSegments[0] + segId] + "%");
			    System.out.println("SWATH POINTS: ");
			}

			String[] segment = new String[3];

			Date segmentStart = sdf.parse(segStart[segId]);
			Date segmentStop = sdf.parse(segStop[segId]);
			if (segmentStart.after(segmentStop)) {
			    segment[0] = segStop[segId];
			    segment[1] = segStart[segId];
			    segmentStart = sdf.parse(segStop[segId]);
			    segmentStop = sdf.parse(segStart[segId]);
			} else {
			    segment[0] = segStart[segId];
			    segment[1] = segStop[segId];
			}

			segment[2] = Double.toString(coverage[(int) nbSegments[0] + segId]);

			study.addSegment(segment);

			double[][] swathPoints = new double[4][2];

			try {
			    swathPoints = this.getSwathRectangle(segStart[segId], segStop[segId]);
			} catch (Exception e) {
			    study.setResult(Feasibility.NOT_FEASIBLE);
			    study.setDescription("Swath can not be loaded. ");
			    return study;
			}

			if (swathPoints == null) {
			    study.setResult(Feasibility.NOT_FEASIBLE);
			    study.setDescription("CFI's can not be loaded. ");
			    return study;
			}

			study.addSwath(swathPoints);

			if (sdf.parse(segStart[segId]).before(sdf.parse(segStop[segId])) && (lastsegment == null || sdf.parse(segStop[segId]).after(lastsegment))) {
			    lastsegment = sdf.parse(segStop[segId]);
			} else if (lastsegment == null || sdf.parse(segStart[segId]).after(lastsegment)) {
			    lastsegment = sdf.parse(segStart[segId]);
			}

			segId++;
		    }

		    study.setResult(Feasibility.FEASIBLE);
		    if (messages) {
			System.out.println("----------------------------------------");
		    }

		}

		if (!valid && tasks != null && tasks.size() != 0) {
		    String response = "Task not feasible. The sensor " + sensor.getSensorId() + " is in use by the ";
		    if (tasks.size() > 1)
			response = response + "tasks: ";
		    else
			response = response + "task: ";
		    for (String task : tasks)
			response = response + task + ", ";
		    response = (response + ".").replace(", .", ". ");

		    study.setResult(Feasibility.NOT_FEASIBLE);
		    study.setDescription(response);
		} else if (!valid) {
		    study.setResult(Feasibility.NOT_FEASIBLE);
		    study.setDescription("Task not feasible. The number of segments is 0 for the occurrence " + (i + 1) + ". ");
		}
	    }

	    if (messages) {
		System.out.println("");
	    }
	} catch (Exception e) {

	    study.setResult(Feasibility.NOT_FEASIBLE);
	    study.setDescription("Task not feasible");
	    e.printStackTrace();
	}
	return study;

    }

    private void assertFeasibilityParams() throws Exception {

	if (this.elevationIncidenceAngle == null) {
	    throw new Exception("elevationIncidenceAngle cannot be null");
	}
	if (this.instrumentMode == null) {
	    throw new Exception("instrumentMode cannot be null");
	}
	if (this.regionOfInterest_lat == null || this.regionOfInterest_lon == null) {
	    throw new Exception("regionOfInterest cannot be null");
	}
	if (this.regionOfInterest_lat.length != this.regionOfInterest_lon.length) {
	    throw new Exception("latitudes and longitudes of the region of interest must have the same length");
	}
	if (sensor.getSensorId() == null) {
	    throw new Exception("sensorID cannot be null");
	}
	if (this.surveyPeriod == null) {
	    throw new Exception("surveyPeriod cannot be null");
	}
	if (this.swathFile == null) {
	    throw new Exception("swathFile cannot be null");
	}
	if (sensor.getOsf_file() == null) {
	    throw new Exception("osfFile cannot be null.");
	}
    }

    /**
     * @return latitudes of the region of interest
     */
    public double[] getRegionOfInterest_lat() {
	return regionOfInterest_lat;
    }

    /**
     * @param regionOfInterest_lat
     *            latitudes of the region of interest to set
     */
    public void setRegionOfInterest_lat(double[] regionOfInterest_lat) {
	this.regionOfInterest_lat = regionOfInterest_lat;
    }

    /**
     * @return longitudes of the region of interest
     */
    public double[] getRegionOfInterest_lon() {
	return regionOfInterest_lon;
    }

    /**
     * @param regionOfInterest_lon
     *            longitudes of the region of interest to set
     */
    public void setRegionOfInterest_lon(double[] regionOfInterest_lon) {
	this.regionOfInterest_lon = regionOfInterest_lon;
    }

    /**
     * @return the elevationIncidenceAngle
     */
    public double[] getElevationIncidenceAngle() {
	return elevationIncidenceAngle;
    }

    /**
     * @param elevationIncidenceAngle
     *            the elevationIncidenceAngle to set
     */
    public void setElevationIncidenceAngle(double[] elevationIncidenceAngle) {
	this.elevationIncidenceAngle = elevationIncidenceAngle;
    }

    /**
     * @return the instrumentMode
     */
    public String getInstrumentMode() {
	return instrumentMode;
    }

    /**
     * @return the new feasibilityId
     */
    public static String getNewFeasibilityId() {
	String result;
	DateTime date = new DateTime();
	result = "http://www.deimos-space.com/sps/" + DateTimeFormat.formatIso(date.getJulianTime(), 0);
	return result;
    }

    /**
     * @param instrumentMode
     *            the instrumentMode to set
     */
    public void setInstrumentMode(String instrumentMode) {
	this.instrumentMode = instrumentMode;
    }

    /**
     * @return the polarizationMode
     */
    public String getPolarizationMode() {
	return polarizationMode;
    }

    /**
     * @param polarizationMode
     *            the polarizationMode to set
     */
    public void setPolarizationMode(String polarizationMode) {
	this.polarizationMode = polarizationMode;
    }

    /**
     * @return the surveyPeriod
     */
    public String[] getSurveyPeriod() {
	return surveyPeriod;
    }

    /**
     * @param surveyPeriod
     *            the surveyPeriod to set
     */
    public void setSurveyPeriod(String[] surveyPeriod) {
	this.surveyPeriod = surveyPeriod;
    }

    /**
     * @return the Swath file
     */
    public String getSwathFile() {
	return swathFile;
    }

    /**
     * @param swathFile
     *            the Swath file to set
     */
    public void setSwathFile(String swathFile) {
	this.swathFile = swathFile;
    }

    /**
     * Enable progress printouts
     */
    public void enableMessages(boolean enable) {
	this.messages = enable;
    }

    /**
     * Obtains the Swath name from the name of the Swath file
     * 
     * @param swathFile
     *            the complete path of the Swath file, e.g.
     *            <i>/home/HMA/ropFiles/SSC1_R_____TS_L_SWATH</i>
     * @return the Swath name, e.g. <i>SSC1_R</i>
     */
    private String getSwathName(String swathFile) {
	String[] tmpString;
	String swathName;
	tmpString = swathFile.split("[/]");
	swathName = tmpString[tmpString.length - 1];

	return swathName;
    }

    private double[][] getSwathRectangle(String startSegment, String stopSegment) throws Exception {
	double[][] swathRectangle = new double[4][2];
	double[] lat_swath = new double[3];
	double[] lon_swath = new double[3];

	if (this.swathFile == null)
	    return null;

	//
	// Generate swath points for START SEGMENT
	//
	long err_swath = cfiw.GetSwathPoints(this.swathFile, startSegment, lat_swath, lon_swath);

	if (err_swath == 0) {

	    //
	    // Left swath point for Start Time
	    //

	    swathRectangle[0][0] = lon_swath[0];
	    swathRectangle[0][1] = lat_swath[0];

	    // Mid swath point lat_swath[1], lon_swath[1] is ignored

	    //
	    // Right swath point for Start Time
	    //

	    swathRectangle[1][0] = lon_swath[2];
	    swathRectangle[1][1] = lat_swath[2];

	    if (messages) {
		System.out.println("Start segment: ");
		System.out.println(lon_swath[0] + "," + lat_swath[0]);
		System.out.println(lon_swath[2] + "," + lat_swath[2]);
	    }

	} else {
	    throw new Exception("Error while getting Start Swath Points");
	}

	//
	// Generate swath points for STOP SEGMENT
	//
	err_swath = cfiw.GetSwathPoints(this.swathFile, stopSegment, lat_swath, lon_swath);

	if (err_swath == 0) {

	    //
	    // Left swath point for Start Time
	    //

	    swathRectangle[3][0] = lon_swath[0];
	    swathRectangle[3][1] = lat_swath[0];

	    // Mid swath point lat_swath[1], lon_swath[1] is ignored

	    //
	    // Right swath point for Start Time
	    //

	    swathRectangle[2][0] = lon_swath[2];
	    swathRectangle[2][1] = lat_swath[2];
	    if (messages) {
		System.out.println("Stop segment: ");
		System.out.println(lon_swath[0] + "," + lat_swath[0]);
		System.out.println(lon_swath[2] + "," + lat_swath[2]);
	    }

	} else {
	    throw new Exception("Error while getting Stop Swath Points");
	}

	return swathRectangle;
    }

    private double[] reverse(double[] array) {
	double[] aux = new double[array.length];
	for (int i = 0, j = array.length - 1; i < array.length; i++, j--) {
	    aux[i] = array[j];
	}
	return aux;
    }

}
